<?php

    /**
     * This file is part of the Intelligencer package.
     * For the full copyright and license, please view the LICENSE
     * file that was distributed with this source code.
     * 
     * @author      drapeko <roman.drapeko@gmail.com>
     * @copyright   Roman Drapeko <roman.drapeko@gmail.com>, 2009 - present
     */

    /**
     * Complex rules extend the functionality of SimpleRules. In SimpleRules case you can add just a file or a list of files.
     * Complex rules allow you to write instructions like "Add recursively all "php" and "class.php" files of the particular directory 
     * excluding file A.php, file B.php and directory C". This gives you an oportunity to write powerful instructions just with a couple of lines.
     * 
     * @author drapeko
     */
    class itComplexRules extends itRules implements itSimpleRulesConvertibleI {
        
        protected $basePath = '';
        protected $plusDirs = array();
        protected $plusRecursiveDirs = array();
        protected $plusFiles = array();
        protected $minusDirs = array();
        protected $minusFiles = array();
        protected $extensions = array('php');
        protected $strictExtensions = false;
        protected $dotDirEnabled = false;

        /**
         * Sets the prefix that will be added to all locations when you call getClass()
         * @param $basePath
         * @return itClassRules itself
         */
        public function setPrefix($basePath) {
            $this->basePath = $basePath;
            return $this;
        }
        
        /**
         * Returns the prefix
         * @return string
         */
        public function getPrefix() {
            return $this->basePath;
        }
        
        /**
         * Adds the instruction to include the whole directory into the rules. Directory can be added recursively.
         * @param string $location
         * @param boolean $recursive
         * @return itComplexRules itself
         */
        public function plusDir($location, $recursive = false) {
            if ($recursive) {
                $this->plusRecursiveDirs[] = $location;
            } else {
                $this->plusDirs[] = $location;
            }
            return $this;
        }
        
        /**
         * Adds the instruction to exclude the directory from the rules.
         * @param string $location
         * @return itComplexRules itself
         */
        public function minusDir($location) {
            $this->minusDirs[] = $location;
            return $this;
        }
        
        /**
         * Adds the instruction to include the file into the rules.
         * @param string $location
         * @return itComplexRules itself
         */
        public function plusFile($location) {
            $this->plusFiles[] = $location;
            return $this;
        }
        
        /**
         * Adds the instruction to exclude the file from the rules.
         * @param string $location
         * @return itComplexRules itself
         */
        public function minusFile($location) {
            $this->minusFiles[] = $location;
            return $this;
        }
        
        /**
         * Adds the instruction to include files only with the following extensions. By default 'php' extension is used.
         * You can use both 'php' and '.php' as an extension.
         * 
         * @param string $location
         * @return itComplexRules itself
         */
        public function setExtensions($extensions) {
            $this->extensions = (array)$extensions;
            return $this;
        }
        
        /**
         * Returns the extensions
         * @return array
         */
        public function getExtensions() {
            return $this->extensions;
        }
        
        /**
         * Sets whether extensions should be strict. If the strict extensions mode is set Intelligencer will treat the characters till the first dot.
         * 
         * For example file A.class.php won't be included in the below case:
         * <code>
         *   $rules = new itComplexRules();
         *   $rules->setExtensions('php')->setStrictExtensions();
         * </code>
         * 
         * And will be included in the following case:
         * <code>
         *   $rules = new itComplexRules();
         *   $rules->setExtensions('php')->setStrictExtensions(false);
         * </code>
         * 
         * @param boolean $val
         * @return itComplexRules itself
         */
        public function setStrictExtensions($val = true) {
            $this->strictExtensions = $val;
            return $this;
        }
        
        /**
         * Returns whether strict extensions mode is set
         * @return boolean
         */
        public function isStrictExtensions() {
            return $this->strictExtensions;
        }
        
        /**
         * Sets the instruction to enable/disable directories starting with dots. By default these directories are disabled.
         * @param boolean $val
         * @return itComplexRules itself
         */
        public function setDotDirEnabled($val = true) {
            $this->dotDirEnabled = $val;
            return $this;
        }
        
        /**
         * Returns whether dot directories are enabled
         * @return boolean
         */
        public function isDotDirEnabled() {
            return $this->dotDirEnabled;
        }
        
        /**
         * (non-PHPdoc)
         * @see Intelligencer/Rules/itSimpleRulesConvertibleI#toSimpleRules()
         */
        public function toSimpleRules() {
            $plusFiles = $this->preFiles($this->plusFiles);
            $minusFiles = $this->preFiles($this->minusFiles, $plusFiles); // plus wins against minus
            $plusDirs = $this->preDirs($this->plusDirs);
            $plusRecursiveDirs = $this->preDirs($this->plusRecursiveDirs);
            $minusDirs = $this->preDirs($this->minusDirs, array_merge($plusDirs, $plusRecursiveDirs)); // plus wins against minus
            
            $result = array();
            $processedRecursiveDirs = array();
            $this->formatExtensions();
            
            foreach($plusRecursiveDirs as $dir) {
                $this->scan($dir, true, $result, $processedRecursiveDirs, $minusDirs);
            }
            
            foreach($plusDirs as $dir) {
                $this->scan($dir, false, $result, $processedRecursiveDirs, $minusDirs);
            }
            
            $result = itUtil::minusArrays($result, $minusFiles);
            $result = array_merge($result, $plusFiles);

            $simpleResult = new itSimpleRules();
            $simpleResult->addFiles($result);
            return $simpleResult;
        }

        protected function scan($dir, $recursive, &$result, &$processedRecursiveDirs, &$minusDirs) {
            $files = scandir($dir);
            foreach($files as $file) {

                if ($file == '.' || $file == '..' || (!$this->dotDirEnabled && $file[0] == '.')) {
                    continue;
                }
                
                $currentFile = $dir.DIRECTORY_SEPARATOR.$file;
                if (is_readable($currentFile)) {
                    if (is_dir($currentFile) && $recursive) {
                        // if this dir in black list or if it was already taken into account
                        $currentFile = itUtil::getRealPath($currentFile);
                        if (!in_array($currentFile, $processedRecursiveDirs)) {
                            if (!in_array($currentFile, $minusDirs)) {
                                $this->scan($currentFile, $recursive, $result, $processedRecursiveDirs, $minusDirs);
                                $processedRecursiveDirs[] = $currentFile;
                            }
                        } 
                    } elseif (is_file($currentFile) && $this->isFileOk($currentFile)) {
                        $result[] = itUtil::getRealPath($currentFile);
                    }
                }
            }
        }
        
        protected function formatExtensions() {
            $this->formattedExtensions = array();
            foreach($this->extensions as $ex) {
                $this->formattedExtensions[] = preg_replace(array("/^\./"), '', $ex);
            }
        }
        
        protected function isFileOk($file) {
            foreach($this->formattedExtensions as $extension) {
                if ($this->strictExtensions) {
                    if (preg_match("/^[^\.]*\.$extension$/", $file)) {
                        return true;
                    }
                }  else {
                    if (preg_match("/\.$extension$/", $file)) {
                        return true;
                    }
                }
            }
            return false;
        } 
        
        /**
         * 1. Excludes non-existent files
         * 2. files that exist in the second array parameter
         * 3. absolute path
         */
        protected function preFiles($files, $exclude = array()) {
            $result = array();
            
            foreach($files as $file) {
                $path = $this->basePath.$file;
                if (is_file($path) && is_readable($path)) {
                    $path = itUtil::getRealPath($path);
                    if (!array_key_exists($path, $exclude)) {
                        $result[] = $path;
                    }
                }
            }
            
            return $result;
        }

        /**
         * 1. Excludes non-existent files
         * 2. files that exist in the second array parameter
         * 3. absolute path
         */
        protected function preDirs($dirs, $exclude = array()) {
            $result = array();
            
            foreach($dirs as $dir) {
                $path = $this->basePath.$dir;
                if (is_dir($path) && is_readable($path)) {
                    $path = itUtil::getRealPath($path);
                    if (!array_key_exists($path, $exclude)) {
                        $result[] = $path;
                    }
                }
            }
            
            return $result;
        }

    }
    
?>